We will create a side panel extension that summarizes Google search results page. ![[Pasted image 20250329034432.png]] And if the user is NOT on a Google search results page: ![[Pasted image 20250329170334.png]] ## Setup Create icon files set and manifest.json file. You should know how by now. Create manifest.json with these contents: ``` { "manifest_version": 3, "name": "", "version": "1.0", "description": ".", "author": "Weng Fei Fung", "icons": { "16": "icon16x16.png", "32": "icon32x32.png", "48": "icon48x48.png", "128": "icon128x128.png" }, "content_security_policy": { "extension_pages": "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; object-src 'self';" }, "content_scripts": [ { "matches": [""], "js": ["content.js"], "run_at": "document_end" } ], "permissions": [ "sidePanel", "scripting", "tabs", "activeTab" ], "action": { "default_icon": "icon.png" }, "background": { "service_worker": "background.js" }, "side_panel": { "default_path": "sidepanel.html" }, "host_permissions": [ "https://*/*", "http://*/*" ] } ``` ^Notice the "sidePanel" as a permission. Otherwise, side panel api would not be available for you to programmatically open the side panel. Create content.js: ``` const currentUrl = window.location.href; if(currentUrl.includes("google.com") && currentUrl.includes("?q=")){ // Here edit DOM of webpage if needed: // ... chrome.runtime.sendMessage({action: "cn2bg-is-google-search-results-page"}); chrome.runtime.sendMessage({action: "cn2sp-enable-search-pagination"}); } else { chrome.runtime.sendMessage({action: "cn2sp-not-google-search-results-page"}); chrome.runtime.sendMessage({action: "cn2sp-disable-search-pagination"}); } ``` Create sidepanel.html: ``` Options

Side Panel


Make sure google search results are open. If they are, hit "Refresh" button at the top right.
``` Create sidepanel.css that sidepanel.html loads in: ``` #controls { position: absolute; top: 10px; right: 10px; display: flex; flex-flow: column nowrap; align-items: center; gap: 2px; } #prevButton.disabled { opacity: 0.5; pointer-events: none; } #nextButton.disabled { opacity: 0.5; pointer-events: none; } ``` Create sidepanel.js that sidepanel.html loads in: ``` chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { console.log("Message received in sidepanel:", message); switch (message.action) { case "bg2sp-render-google-search-results-summary": console.log("Message of dynamic data received in sidepanel:", message); summarizeGoogleSearchResults(message); break; case "cn2sp-not-google-search-results-page": console.log("Message of not google search results received in sidepanel:", message); showFailNotGoogleSearchResults(message); break; case "cn2sp-enable-search-pagination": console.log("Message of enable search pagination received in sidepanel:", message); document.getElementById("prevButton").classList.remove("disabled"); document.getElementById("nextButton").classList.remove("disabled"); break; case "cn2sp-disable-search-pagination": console.log("Message of disable search pagination received in sidepanel:", message); document.getElementById("prevButton").classList.add("disabled"); document.getElementById("nextButton").classList.add("disabled"); break; } }); function summarizeGoogleSearchResults(message) { console.log("Message of google search results received in sidepanel:", message); if(!message?.payload) { console.log("No payload received in sidepanel:", message); return; } const searchResults = message.payload; const reportEl = document.getElementById("report") reportEl.innerHTML = ""; reportEl.innerHTML += `Search Results:
`; /** * * searchResults: Array * * searchResultObject = { * title: string, * url: string, * description: string * } * */ searchResults.forEach(searchResult=>{ // console.log(searchResult); const {title, url, description} = searchResult; reportEl.innerHTML += `${title}
`; reportEl.innerHTML += `${description}

`; }); } function showFailNotGoogleSearchResults(message) { console.log("Message of NOT google search results received in sidepanel:", message); const reportEl = document.getElementById("report") reportEl.innerHTML = ""; reportEl.innerHTML += `
Not Google Search Results!

Search for something on Google, then click "Refresh" button at the top right of this side panel.
`; } document.addEventListener("DOMContentLoaded", function() { const refreshButton = document.getElementById("refreshButton"); const prevButton = document.getElementById("prevButton"); const nextButton = document.getElementById("nextButton"); refreshButton.addEventListener("click", function() { chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { chrome.tabs.reload(tabs[0].id); }); }); prevButton.addEventListener("click", function() { if(prevButton.classList.contains("disabled")) { return; } chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { chrome.scripting.executeScript({ target: {tabId: tabs[0].id}, func: () => { let paginationControls = null; document.querySelectorAll('h1').forEach(h1=>{ if(h1.textContent.includes('Page Navigation')) { paginationControls = h1.parentElement; } }); if (paginationControls) { const aEls =paginationControls.querySelectorAll('a'); const prevLink = aEls[0]; if(prevLink) { prevLink.click(); } } } }); }); // end of chrome.tabs.query }); nextButton.addEventListener("click", function() { if(nextButton.classList.contains("disabled")) { return; } chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { chrome.scripting.executeScript({ target: {tabId: tabs[0].id}, func: () => { let paginationControls = null; document.querySelectorAll('h1').forEach(h1=>{ if(h1.textContent.includes('Page Navigation')) { paginationControls = h1.parentElement; } }); if (paginationControls) { const aEls = paginationControls.querySelectorAll('a'); const nextLink = aEls[aEls.length-1]; if(nextLink) { nextLink.click(); } } } }); }); // end of chrome.tabs.query }); }); ``` Create background.js... Which helps coordinate the user clicking the chrome extension icon and the stats loading in the sidepanel that subsequently finishes loading (page needs to finish loading before we start writing to it): ``` console.log("Background script loaded"); chrome.runtime.onInstalled.addListener(() => { chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); }); chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { if (message.action === "cn2bg-is-google-search-results-page") { chrome.tabs.query({active: true, currentWindow: true}, async function(tabs) { const activeTab = tabs[0]; // Not needed, because content.js already checked for this // if (activeTab.url.includes('google.com') && activeTab.url.includes('?q=')) { // Google search page console.log("Google search page"); /** * * searchResults:Array * * searchResultObject = { * title: string, * url: string, * description: string * } * */ const executedResults = await chrome.scripting.executeScript({ target: {tabId: activeTab.id}, func: () => { var searchResults = []; document.getElementById("main").querySelectorAll("h3").forEach(h3=>{ var title = h3.innerText; // console.log(title); if(!h3?.closest("[data-snf]")?.parentElement) return true; var lines = h3.closest("[data-snf]").parentElement.querySelectorAll("[data-snf]"); // Lines are: everything above description, description, site further links if available var description = lines[1]?.innerText; var url = lines[0]?.querySelector("a")?.href; // console.log(description); searchResults.push({ title, url, description }); }); return searchResults; } }); var searchResults = executedResults[0].result; chrome.runtime.sendMessage({action: "bg2sp-render-google-search-results-summary", payload: searchResults}); }); } }); ``` --- ## Discussion **How the logic generally is:** 1. When user clicks the chrome extension icon, under the hood, the background.js listens for and responds to onClicked by opening the sidepanel with `chrome.sidePanel.open` at the current window. This is all done without imperative programming (instruct every step of an implementation). Instead, you did declarative programming (told what it is) when you told Chrome Extension how to behave between extension icon clicks and the side panel behavior at a chrome extension install event: `chrome.runtime.onInstalled.addListener`.... `sidePanel.setPanelBehavior({ openPanelOnActionClick: true })` (truncated). 2. When sidepanel.html near finishes loading, its sidepanel.js begins listening for messages. Our sidepanel will be the main relay for many types of messages because there are different renderings of sidepanel possible and sidepanel can receive a message on what to render immediately. The possible messages to sidepanel are broken down into (for atomic render functions): - not google search results message, - google search results format, - disabled Prev/Next buttons (for Google search results page), - enabled Prev/Next button. 3. The background.js will be a secondary relay whose purpose is to get the signal from content.js that it is google search results so help parse the DOM for information to send with a message to sidepanel.js for rendering google search results extension. While content.js could parse its own DOM whether than using executeScript inside background.js, we want content.js to be more of a: (Refer to next number) 4. Our content.js is the initiator of the messages, quickly checking on frontend (the website at the active tab) whether the url (`window.location.href`) is a google search results url. The Prev/Next button at the Chrome Extension side panel is for the Prev and Next at the Google Search Results: - Side panel's Prev/Next: ![[Pasted image 20250329170058.png]] - Would be equivalent to clicking "Previous/Next" on the page: ![[Pasted image 20250329170019.png]] ![[Chrome-extension-sidepanel-next.gif]] **The key js files can be thought of as a separation of concerns:** - content.js: URL-based message initiator - background.js: Content parser for google search results - sidepanel.js: Renderer based on message. Is unaware what combinations of renders means "good-to-go" because all render functions are atomic. **Conventions:** - When you see a message like `cn2bg-is-google-search-results-page`, look to the prefix. The prefix here is "cn2bg" and that means when you see this code, you're either at content.js sending the message or you're at background.js receiving the message. This naming convention for messages is not necessary but helps. **How the logic flows:** 1. When web content is done loading at active tab, content.js checks the url for "google.com" and "?q=". 1. If found, content.js sends message "cn2bg-is-google-search-results-page" and "cn2sp-enable-search-pagination" globally 1. Background picks up first "good-to-go" message, then it executesScript on the active tab's web content for the title, url, and description. And finally, backgrounds send message "bg2sp-render-google-search-results-summary" 1. Sidepanel picks up message. It runs summarizeGoogleSearchResults, which renders useful information about Google search results on the sidepanel. 2. Sidepanel picks up the second message which is a "good-to-go" message too, and it makes sure they are enabled - the Prev and Next button for browsing search results page. As such, this second message piped directly from content.js to sidepanel.js instead of having an intermediate background.js to relay a chained message. 2. If no found in url that is characteristics of a google search results page, content.js sends messages directly to sidepanel.js (because background.js not needed to parse DOM, and remember content.js sole responsibility is just to initiate messages based on url rather than perform parsing as well). The messages are "cn2sp-not-google-search-results-page" and "cn2sp-disable-search-pagination". 1. SIdepanel picks up both messages, then it renders a message that this page isn't google search results page (sidepanel.js:`showFailNotGoogleSearchResults()`) and make sure the Prev and Next buttons for Google search results page navigation are disabled. **Two important lessons here is that:** - We've proven that content.js and background.js can parse the web content at the active tab. The previous challenge on creating side panel that summarizes the web content AND this challenge on creating side panel only for google search results page - had both used background.js to parse the web contents for separation of concern reasons, BUT because this challenge could parse the URL from content.js, then that means it could have parsed the DOM at content.js too. - We've proven different methods are required to get the url based on the context you're in. - In the previous challenge, there was no content.js but we get the url of the web content so that we can summarize the page we are on in the side panel. It summarized the url by using `activeTab.title` where activeTab is `tabs[0]` which was scoped from `chrome.tabs.query({active: true, currentWindow: true}, async function(tabs)`. In this challenge, we do have a content.js, so we check the url with `window.location.href` at content.js - In summary: If getting url of the web content at the active tab and you're in content.js, you can check directly with window.location.href, else if outside the content.js context, then you have to run chrome.tabs.query to get the active tab's title. **Mermaid diagram (fyi):** ``` sequenceDiagram participant User participant ChromeExtension as Chrome Extension (icon) participant Content as content.js participant BackgroundJS as background.js participant SidePanel as sidepanel.html + sidepanel.js participant ActiveTab as Active Tab User->>ChromeExtension: Clicks extension icon ChromeExtension->>BackgroundJS: onClicked event BackgroundJS->>SidePanel: chrome.sidePanel.open() Content->>Content: Check url Note over Content,SidePanel: Is Google Search Results Page at Active Tab Content->>BackgroundJS: Is google search results page so content messages background to parse data:
`cn2bg-is-google-search-results-page` BackgroundJS-->>ActiveTab: Background asks for tab ID of active tab in order to parse web content from the correct tab ActiveTab-->>BackgroundJS: BackgroundJS-->>Content: Background performs executeScript to parse web content Content-->>BackgroundJS: BackgroundJS-->>SidePanel: Parsed data from google search results page is sent to side panel
(to render summary of search results) via the message:
`bg2sp-render-google-search-results-summary` SidePanel<<-->>SidePanel: Render the summary of Google Search Result Page Content->>SidePanel: Is google search results so content messages side panel to enable prev/next buttons:
`cn2sp-enable-search-pagination` SidePanel<<-->>SidePanel: Make sure Prev/Next buttons are enabled Note over Content,SidePanel: Is NOT Google Search Results Page at Active Tab Content->>SidePanel: Is NOT google search results so content messages side panel to show warning message:
`cn2sp-not-google-search-results-page` SidePanel<<-->>SidePanel: Show message that this extension won't
work for non-google search results page. Content->>SidePanel: Is NOT google search results so content messages side panel to disable prev/next buttons:
`cn2sp-disable-search-pagination` SidePanel<<-->>SidePanel: Make sure Prev/Next buttons are disabled3 ``` ![[Pasted image 20250329163927.png]] --- ## Testing Update/install the chrome extension. Then at google.com, search for a term (eg. "test") and submit it. Finally, click the chrome extension icon to open the side panel and see summary. ![[Pasted image 20250329034432.png]]